package se.webbzon.boltzmann.game.object.action;

import java.awt.Color;
import java.util.Collection;

import se.webbzon.boltzmann.collector.Collector;
import se.webbzon.boltzmann.collector.CollectorListener;
import se.webbzon.boltzmann.environment.AbstractEnvironment;
import se.webbzon.boltzmann.game.object.player.AbstractPlayerCharacter;
import se.webbzon.boltzmann.input.Input;
import se.webbzon.boltzmann.input.Button;
import se.webbzon.boltzmann.input.InputListener;
import se.webbzon.oschi01.worldarea.WorldArea;
import se.webbzon.oschi01.worldobject.WorldObject;
import se.webbzon.oschi01.worldobject.WorldStaticObject;
import se.webbzon.oschi01.worldobject.appearance.Appearance3D;
import se.webbzon.oschi01.worldobject.appearance.ConeAppearance3D;
import se.webbzon.oschi01.worldobject.appearance.SimpleAppearance3D;

public abstract class AbstractAction<T extends WorldStaticObject> extends WorldStaticObject {
	
	// The environment in which the abstract action is created
	private final AbstractEnvironment env;
	
	// The input listener used for this abstract action
	private final InputListener inputListener;
	
	// The collector listener used for this 
	private final CollectorListener<AbstractPlayerCharacter> collectorListener;
	
	// Whatever this abstract action only accepts activation from the front
	private boolean frontOnly;
	
	// The phase of the action
	private double t;
	
	// The parent instance
	private T parent;
		
	// The maximal range of the action
	private double range;
	
	// Whatever the closes player was within the given range or not
	// during the last step.
	private boolean playerWithinRange;
	
	// A collector of all player instances
	private volatile Collector<AbstractPlayerCharacter> playerCollector;
	
	// The active state of the action
	private volatile boolean active;
	
	// The player which invoked the last action
	private volatile AbstractPlayerCharacter invokedBy;
	
	/*============================================================
	Constructors
	============================================================*/
	
	/** Creates a new abstract action which will hover above a given
	 * parent. The abstract action will only be visible if any of the 
	 * players in the given collection in within the given range. **/
	public AbstractAction(	int registeredMethods,
							AbstractEnvironment env,
							T parent,
							Color color,
							boolean frontOnly, double range) {
		super(	registeredMethods | WorldObject.onCreate + 
				WorldObject.onStep + WorldObject.onDestroy,
				0,createAction(color));
		this.env = env;
		this.parent = parent;
		this.range = range;
		this.frontOnly = frontOnly;
		t = 0;
		active = true;
		playerWithinRange = false;
		inputListener = new InputListenerImpl();
		collectorListener = new CollectorListenerImpl();
		
		if (env.isDebugMode())
			setVisible(true);
		else
			setVisible(playerWithinRange);
		setSize(-6,-6,6);
	}
	
	/*============================================================
	Public Methods
	============================================================*/
	
	/** Triggers the given action associated with this abstract action. **/
	public abstract void onActionBegin(T parent, AbstractPlayerCharacter player);
	
	/** Returns the environment in which this action is created. **/
	public AbstractEnvironment getEnvironment() {
		return env;
	}
	
	/** Returns the color of this action. **/
	public Color getColor() {
		SimpleAppearance3D appearance = 
				(SimpleAppearance3D) getAppearance();
		return appearance.getColor();
	}
	
	/** Sets the color of this action. **/
	public void setColor(Color color) {
		SimpleAppearance3D appearance = 
				(SimpleAppearance3D) getAppearance();
		appearance.setColor(color);
	}
	
	/** Returns true if this abstract action only can be activated 
	 * from the front. **/
	public boolean getFrontOnly() {
		return frontOnly;
	}
	
	/** Sets whatever this abstract action only can be activated from the 
	 * front. **/
	public void setFrontOnly(boolean frontOnly) {
		this.frontOnly = frontOnly;
	}
	
	/** Returns true if this abstract action is active. **/
	public boolean getActive() {
		return active;
	}
	
	/** Sets whatever this action is active or not. **/
	public void setActive(boolean active) {
		this.active = active;
		setVisible(active && playerWithinRange);
	}
	
	/** Sets the parent of this action. **/
	public void setParent(T parent) {
		this.parent = parent;
		updatePosition(t);
	}
	
	/** Returns the parent of this action. **/
	public T getParent() {
		return parent;
	}
	
	/** Sets the range of this action. **/
	public void setRange(double range) {
		this.range = range;
	}
	
	/** Returns the range of this action. **/
	public double getRange() {
		return range;
	}
	
	/** Returns the closest player to the parent of this abstract action. **/
	public AbstractPlayerCharacter closestPlayer() {
		AbstractPlayerCharacter closestPlayer = null;
		double minDistance = Double.MAX_VALUE;
		Collection<AbstractPlayerCharacter> players = playerCollector.getInstances();
		synchronized(players) {
			for (AbstractPlayerCharacter player : players) {
				final double distance = parent.distanceTo(player);
				if (distance < minDistance) {
					closestPlayer = player;
					minDistance = distance;
				}
			}
		}
		return closestPlayer;
	}
	
	@Override public void onCreate(WorldArea world) {
		playerCollector = Collector.getCollector(world, AbstractPlayerCharacter.class);
		playerCollector.addListener(collectorListener);
		Collection<AbstractPlayerCharacter> players = playerCollector.getInstances();
		synchronized(players) {
			for (AbstractPlayerCharacter player : players)
				player.getPlayer().getInput().addListener(inputListener);
		}
		updatePosition(t);
	}
	
	@Override public void onStep(WorldArea world) {
		if (invokedBy != null) {
			if (!invokedBy.isBlocked()) {
				setActive(false);
				onActionBegin(parent,invokedBy);
			}
			// Reset invokedBy so new players can use the action
			invokedBy = null;
		} else if (active && parent != null && parent.exists()) {
			updatePosition(t + Math.PI/60);
		
			final AbstractPlayerCharacter player = closestPlayer();
			if (player != null) {
				if (availableFor(player) && !playerWithinRange) {
					playerWithinRange = true;
					setVisible(playerWithinRange);
				} else if (!availableFor(player) && playerWithinRange) {
					playerWithinRange = false;
					setVisible(playerWithinRange);
				}
			}
		}
	}
	
	@Override public void onDestroy(WorldArea world) {
		Collection<AbstractPlayerCharacter> players = playerCollector.getInstances();
		synchronized(players) {
			for (AbstractPlayerCharacter player : players)
				player.getPlayer().getInput().removeListener(inputListener);
		}
		playerCollector.removeListener(collectorListener);
		playerCollector = null;
	}
	
	/*============================================================
	Private Methods
	============================================================*/
	
	/** Creates the action appearance. **/
	private static Appearance3D createAction(Color color) {
		SimpleAppearance3D cone = new ConeAppearance3D(false,4);
		cone.setColor(color);
		return cone;
	}
	
	/** Updates the position of this abstract action. **/
	private void updatePosition(double newT) {
		if (t != newT) {
			t = newT;
			setRotation(0, 1, 0, t);
		}
		if (parent != null) {
			setPosition(parent.getX(), 
						parent.getY() + 
							parent.getHeight()/2 + 8 + 4 * Math.sin(t), 
						parent.getZ());
		}
	}
	
	/** Returns true if the action is available for the given
	 * instance. **/
	private boolean availableFor(WorldStaticObject instance) {
		if (frontOnly && parent.getZ() > instance.getZ() && parent.isVisible())
			return false;
		else {
			final double distance = parent.distanceTo(instance);
			return distance < range;
		}
	}
	
	/*============================================================
	Private Classes
	============================================================*/
	
	/** An implementation of an input listener. **/
	private final class InputListenerImpl implements InputListener {
	
		@Override public void primaryAxisChanged(	Input input, 
													double direction,
													double value) {};

		@Override public void secondaryAxisChanged(	Input input, 
													double direction,
													double value) {};

		@Override public void buttonPressed(Input input, Button button) {
			if (active && button == Button.ACTION) {
				Collection<AbstractPlayerCharacter> players = playerCollector.getInstances();
				synchronized(players) {
					for (AbstractPlayerCharacter player : players)
						if (	input == player.getPlayer().getInput() &&
								availableFor(player)) {
							invokedBy = player;
							return;
						}
				}
			}
		}

		@Override public void buttonReleased(Input input, Button button) {};
		
		@Override public void bindingAccepted(Input input, Button button) {};
	}
	
	/** An abstract player collector. **/
	private final class CollectorListenerImpl implements CollectorListener<AbstractPlayerCharacter> {

		@Override public void onCreateInstance(	Collector<AbstractPlayerCharacter> collector,
												AbstractPlayerCharacter instance) {
			instance.getPlayer().getInput().addListener(inputListener);
		}

		@Override public void onDestroyInstance(Collector<AbstractPlayerCharacter> collector,
												AbstractPlayerCharacter instance) {
			instance.getPlayer().getInput().removeListener(inputListener);
		}

		@Override public void onDestroyCollector(Collector<AbstractPlayerCharacter> collector) {
			Collection<AbstractPlayerCharacter> players = collector.getInstances();
			synchronized(players) {
				for (AbstractPlayerCharacter player : players)
					player.getPlayer().getInput().removeListener(inputListener);
			}
		}
		
	}

}
